home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Personal Computer World 2005 October
/
PCWOCT05.iso
/
Software
/
FromTheMag
/
XAMPP 1.4.14
/
xampp-win32-1.4.14-installer.exe
/
xampp
/
php
/
pear
/
DB
/
NestedSet.php
< prev
next >
Wrap
PHP Script
|
2004-03-24
|
62KB
|
2,147 lines
<?php
//
// +----------------------------------------------------------------------+
// | PEAR :: DB_NestedSet |
// +----------------------------------------------------------------------+
// | Copyright (c) 1997-2003 The PHP Group |
// +----------------------------------------------------------------------+
// | This source file is subject to version 2.0 of the PHP license, |
// | that is bundled with this package in the file LICENSE, and is |
// | available at through the world-wide-web at |
// | http://www.php.net/license/2_02.txt. |
// | If you did not receive a copy of the PHP license and are unable to |
// | obtain it through the world-wide-web, please send a note to |
// | license@php.net so we can mail you a copy immediately. |
// +----------------------------------------------------------------------+
// | Authors: Daniel Khan <dk@webcluster.at> |
// | Jason Rust <jason@rustyparts.com> |
// +----------------------------------------------------------------------+
// $Id: NestedSet.php,v 1.31 2003/08/13 20:09:29 datenpunk Exp $
//
// CREDITS:
// --------
// - Many thanks to Jason Rust for doing great improvements and cleanup work for the current release
// - Thanks to Kristian Koehntopp for publishing an explanation of the Nested Set
// technique and for the great work he did and does for the php community
// - Thanks to Daniel T. Gorski for his great tutorial on www.develnet.org
// - Thanks to Hans Lellelid for suggesting support for MDB and for helping me with the
// implementation
// ...
// - Thanks to my parents for ... just kidding :]
require_once 'PEAR.php';
// {{{ constants
// Error and message codes
define('NESE_ERROR_RECURSION', 'E100');
define('NESE_DRIVER_NOT_FOUND', 'E200');
define('NESE_ERROR_NOHANDLER', 'E300');
define('NESE_ERROR_TBLOCKED', 'E010');
define('NESE_MESSAGE_UNKNOWN', 'E0');
define('NESE_ERROR_NOTSUPPORTED', 'E1');
define('NESE_ERROR_PARAM_MISSING','E400');
define('NESE_ERROR_NOT_FOUND', 'E500');
// for moving a node before another
define('NESE_MOVE_BEFORE', 'BE');
// for moving a node after another
define('NESE_MOVE_AFTER', 'AF');
// for moving a node below another
define('NESE_MOVE_BELOW', 'SUB');
// }}}
// {{{ DB_NestedSet:: class
/**
* DB_NestedSet is a class for handling nested sets
*
* @author Daniel Khan <dk@webcluster.at>
* @package DB_NestedSet
* @version $Revision: 1.31 $
* @access public
*/
// }}}
class DB_NestedSet extends PEAR {
// {{{ properties
/**
* @var array The field parameters of the table with the nested set. Format: 'realFieldName' => 'fieldId'
* @access public
*/
var $params = array(
'STRID' => 'id',
'ROOTID'=> 'rootid',
'l' => 'l',
'r' => 'r',
'STREH' => 'norder',
'LEVEL' => 'level',
'STRNA' => 'name'
);
/**
* @var array The above parameters flipped for easy access
* @access private
*/
var $flparams = array();
/**
* @var array An array of field ids that must exist in the table
* Not used yet
*/
var $requiredParams = array('id', 'rootid', 'l', 'r', 'norder', 'level');
/**
* @var string The table with the actual tree data
* @access public
*/
var $node_table = 'tb_nodes';
/**
* @var string The table to handle locking
* @access public
*/
var $lock_table = 'tb_locks';
/**
* @var string The table used for sequences
* @access public
*/
var $sequence_table;
/**
* Secondary order field. Normally this is the order field, but can be changed to
* something else (i.e. the name field so that the tree can be shown alphabetically)
* @var string
* @access public
*/
var $secondarySort;
/**
* @var int The time to live of the lock
* @access public
*/
var $lockTTL = 1;
/**
* @var bool Enable debugging statements?
* @access public
*/
var $debug = false;
/**
* @var bool Lock the structure of the table?
* @access private
*/
var $structureTableLock = false;
/**
* @var bool Skip the callback events?
* @access private
*/
var $skipCallbacks = false;
/**
* @var object cache Optional PEAR::Cache object
* @access public
*/
var $cache = false;
/**
* @var bool Do we want to use caching
* @access private
*/
var $_caching = false;
/**
*
* @var bool Temporary switch for cache
* @access private
*/
var $_restcache = false;
/**
* @var array Map of error messages to their descriptions
*/
var $messages = array(
NESE_ERROR_RECURSION => 'This operation would lead to a recursion',
NESE_ERROR_TBLOCKED => 'The structure Table is locked for another database operation, please retry.',
NESE_DRIVER_NOT_FOUND => 'The selected database driver wasn\'t found',
NESE_ERROR_NOTSUPPORTED => 'Method not supported yet',
NESE_ERROR_NOHANDLER => 'Event handler not found',
NESE_ERROR_PARAM_MISSING=> 'Parameter missing',
NESE_MESSAGE_UNKNOWN => 'Unknown error or message',
NESE_ERROR_NOT_FOUND => 'Node not found',
);
/**
* @var array The array of event listeners
* @access public
*/
var $eventListeners = array();
// }}}
// +---------------------------------------+
// | Base methods |
// +---------------------------------------+
// {{{ constructor
/**
* Constructor
*
* @param array $params Database column fields which should be returned
*
* @access private
* @return void
*/
function DB_NestedSet($params)
{
$this->_debugMessage('DB_NestedSet()');
$this->PEAR();
if (is_array($params) && count($params) > 0) {
$this->params = $params;
}
$this->flparams = array_flip($this->params);
$this->sequence_table = $this->node_table . '_' . $this->flparams['id'];
$this->secondarySort = $this->flparams['norder'];
}
// }}}
// {{{ factory
/**
* Handles the returning of a concrete instance of DB_NestedSet based on the driver.
*
* @param string $driver The driver, such as DB or MDB
* @param string $dsn The dsn for connecting to the database
* @param array $params The field name params for the node table
*
* @access public
* @return object The DB_NestedSet object
*/
function & factory($driver, $dsn, $params = array())
{
$driverpath = dirname(__FILE__).'/NestedSet/'. $driver.'.php';
if(!file_exists($driverpath) || !$driver) {
return new PEAR_Error('E200',"The database driver '$driver' wasn't found");
}
include_once($driverpath);
$classname = 'DB_NestedSet_' . $driver;
return new $classname($dsn, $params);
}
// }}}
// {{{ destructor
/**
* PEAR Destructor
* Releases all locks
* Closes open database connections
*
* @access private
* @return void
*/
function _DB_NestedSet()
{
$this->_debugMessage('_DB_NestedSet()');
$this->_releaseLock();
}
// }}}
// +----------------------------------------------+
// | NestedSet manipulation and query methods |
// |----------------------------------------------+
// | Querying the tree |
// +----------------------------------------------+
// {{{ getAllNodes()
/**
* Fetch the whole NestedSet
*
* @param bool $keepAsArray (optional) Keep the result as an array or transform it into
* a set of DB_NestedSet_Node objects?
* @param bool $aliasFields (optional) Should we alias the fields so they are the names
* of the parameter keys, or leave them as is?
* @param array $addSQL (optional) Array of additional params to pass to the query.
*
* @access public
* @return mixed False on error, or an array of nodes
*/
function getAllNodes($keepAsArray = false, $aliasFields = true, $addSQL = array())
{
$this->_debugMessage('getAllNodes()');
$sql = sprintf('SELECT %s %s FROM %s %s %s ORDER BY %s.%s, %s.%s ASC',
$this->_getSelectFields($aliasFields),
$this->_addSQL($addSQL, 'cols'),
$this->node_table,
$this->_addSQL($addSQL, 'join'),
$this->_addSQL($addSQL, 'append'),
$this->node_table,
$this->flparams['level'],
$this->node_table,
$this->secondarySort);
if (!$this->_caching) {
$nodeSet = $this->_processResultSet($sql, $keepAsArray, $aliasFields);
} else {
$nodeSet = $this->cache->call('DB_NestedSet->_processResultSet', $sql, $keepAsArray, $aliasFields);
}
// EVENT (nodeLoad)
foreach (array_keys($nodeSet) as $key) {
$this->triggerEvent('nodeLoad', $nodeSet[$key]);
}
return $nodeSet;
}
// }}}
// {{{ getRootNodes()
/**
* Fetches the first level (the rootnodes) of the NestedSet
*
* @param bool $keepAsArray (optional) Keep the result as an array or transform it into
* a set of DB_NestedSet_Node objects?
* @param bool $aliasFields (optional) Should we alias the fields so they are the names
* of the parameter keys, or leave them as is?
* @param array $addSQL (optional) Array of additional params to pass to the query.
*
* @see _addSQL()
* @access public
* @return mixed False on error, or an array of nodes
*/
function getRootNodes($keepAsArray = false, $aliasFields = true, $addSQL = array())
{
$this->_debugMessage('getRootNodes()');
$sql = sprintf('SELECT %s %s FROM %s %s WHERE %s.%s=%s.%s %s ORDER BY %s.%s ASC',
$this->_getSelectFields($aliasFields),
$this->_addSQL($addSQL, 'cols'),
$this->node_table,
$this->_addSQL($addSQL, 'join'),
$this->node_table,
$this->flparams['id'],
$this->node_table,
$this->flparams['rootid'],
$this->_addSQL($addSQL, 'append'),
$this->node_table,
$this->secondarySort);
if (!$this->_caching) {
$nodeSet = $this->_processResultSet($sql, $keepAsArray, $aliasFields);
} else {
$nodeSet = $this->cache->call('DB_NestedSet->_processResultSet', $sql, $keepAsArray, $aliasFields);
}
// EVENT (nodeLoad)
foreach (array_keys($nodeSet) as $key) {
$this->triggerEvent('nodeLoad', $nodeSet[$key]);
}
return $nodeSet;
}
// }}}
// {{{ getBranch()
/**
* Fetch the whole branch where a given node id is in
*
* @param int $id The node ID
* @param bool $keepAsArray (optional) Keep the result as an array or transform it into
* a set of DB_NestedSet_Node objects?
* @param bool $aliasFields (optional) Should we alias the fields so they are the names
* of the parameter keys, or leave them as is?
* @param array $addSQL (optional) Array of additional params to pass to the query.
*
* @see _addSQL()
* @access public
* @return mixed False on error, or an array of nodes
*/
function getBranch($id, $keepAsArray = false, $aliasFields = true, $addSQL = array())
{
$this->_debugMessage('getBranch($id)');
if (!($thisnode = $this->_getNodeObject($id))) {
return false;
}
$sql = sprintf('SELECT %s %s FROM %s %s WHERE %s.%s=%s %s ORDER BY %s.%s, %s.%s ASC',
$this->_getSelectFields($aliasFields),
$this->_addSQL($addSQL, 'cols'),
$this->node_table,
$this->_addSQL($addSQL, 'join'),
$this->node_table,
$this->flparams['rootid'],
$this->db->quote($thisnode->rootid),
$this->_addSQL($addSQL, 'append'),
$this->node_table,
$this->flparams['level'],
$this->node_table,
$this->secondarySort);
if (!$this->_caching) {
$nodeSet = $this->_processResultSet($sql, $keepAsArray, $aliasFields);
} else {
$nodeSet = $this->cache->call('DB_NestedSet->_processResultSet', $sql, $keepAsArray, $aliasFields);
}
// EVENT (nodeLoad)
foreach (array_keys($nodeSet) as $key) {
$this->triggerEvent('nodeLoad', $nodeSet[$key]);
}
return $nodeSet;
}
// }}}
// {{{ getParents()
/**
* Fetch the parents of a node given by id
*
* @param int $id The node ID
* @param bool $keepAsArray (optional) Keep the result as an array or transform it into
* a set of DB_NestedSet_Node objects?
* @param bool $aliasFields (optional) Should we alias the fields so they are the names
* of the parameter keys, or leave them as is?
* @param array $addSQL (optional) Array of additional params to pass to the query.
*
* @see _addSQL()
* @access public
* @return mixed False on error, or an array of nodes
*/
function getParents($id, $keepAsArray = false, $aliasFields = true, $addSQL = array())
{
$this->_debugMessage('getParents($id)');
if (!($child = $this->_getNodeObject($id))) {
return false;
}
$sql = sprintf('SELECT %s %s FROM %s %s
WHERE %s.%s=%s AND %s.%s<%s AND %s.%s<%s AND %s.%s>%s %s
ORDER BY %s.%s ASC',
$this->_getSelectFields($aliasFields),
$this->_addSQL($addSQL, 'cols'),
$this->node_table,
$this->_addSQL($addSQL, 'join'),
$this->node_table,
$this->flparams['rootid'],
$child->rootid,
$this->node_table,
$this->flparams['level'],
$child->level,
$this->node_table,
$this->flparams['l'],
$child->l,
$this->node_table,
$this->flparams['r'],
$child->r,
$this->_addSQL($addSQL, 'append'),
$this->node_table,
$this->flparams['level']);
if (!$this->_caching) {
$nodeSet = $this->_processResultSet($sql, $keepAsArray, $aliasFields);
} else {
$nodeSet = $this->cache->call('DB_NestedSet->_processResultSet', $sql, $keepAsArray, $aliasFields);
}
// EVENT (nodeLoad)
foreach (array_keys($nodeSet) as $key) {
$this->triggerEvent('nodeLoad', $nodeSet[$key]);
}
return $nodeSet;
}
// }}}
// {{{ getChildren()
/**
* Fetch the children _one level_ after of a node given by id
*
* @param int $id The node ID
* @param bool $keepAsArray (optional) Keep the result as an array or transform it into
* a set of DB_NestedSet_Node objects?
* @param bool $aliasFields (optional) Should we alias the fields so they are the names
* of the parameter keys, or leave them as is?
* @param bool $forceNorder (optional) Force the result to be ordered by the norder
* param (as opposed to the value of secondary sort). Used by the move and
* add methods.
* @param array $addSQL (optional) Array of additional params to pass to the query.
*
* @see _addSQL()
* @access public
* @return mixed False on error, or an array of nodes
*/
function getChildren($id, $keepAsArray = false, $aliasFields = true, $forceNorder = false, $addSQL = array())
{
$this->_debugMessage('getChildren($id)');
$parent = $this->_getNodeObject($id);
if (!$parent || $parent->l == ($parent->r - 1)) {
return false;
}
$orderBy = $forceNorder ? $this->flparams['norder'] : $this->secondarySort;
$sql = sprintf('SELECT %s %s FROM %s %s
WHERE %s.%s=%s AND %s.%s=%s+1 AND %s.%s BETWEEN %s AND %s %s
ORDER BY %s.%s ASC',
$this->_getSelectFields($aliasFields),
$this->_addSQL($addSQL, 'cols'),
$this->node_table,
$this->_addSQL($addSQL, 'join'),
$this->node_table,
$this->flparams['rootid'],
$this->db->quote($parent->rootid),
$this->node_table,
$this->flparams['level'],
$parent->level,
$this->node_table,
$this->flparams['l'],
$parent->l,
$parent->r,
$this->_addSQL($addSQL, 'append'),
$this->node_table,
$orderBy);
if (!$this->_caching) {
$nodeSet = $this->_processResultSet($sql, $keepAsArray, $aliasFields);
} else {
$nodeSet = $this->cache->call('DB_NestedSet->_processResultSet', $sql, $keepAsArray, $aliasFields);
}
// EVENT (nodeLoad)
foreach (array_keys($nodeSet) as $key) {
$this->triggerEvent('nodeLoad', $nodeSet[$key]);
}
return $nodeSet;
}
// }}}
// {{{ getSubBranch()
/**
* Fetch all the children of a node given by id
*
* getChildren only queries the immediate children
* getSubBranch returns all nodes below the given node
*
* @param string $id The node ID
* @param bool $keepAsArray (optional) Keep the result as an array or transform it into
* a set of DB_NestedSet_Node objects?
* @param bool $aliasFields (optional) Should we alias the fields so they are the names
* of the parameter keys, or leave them as is?
* @param array $addSQL (optional) Array of additional params to pass to the query.
*
* @see _addSQL()
* @access public
* @return mixed False on error, or an array of nodes
*/
function getSubBranch($id, $keepAsArray = false, $aliasFields = true, $addSQL = array())
{
$this->_debugMessage('getSubBranch($id)');
if (!($parent = $this->_getNodeObject($id))) {
return false;
}
$sql = sprintf('SELECT %s %s FROM %s %s WHERE %s.%s BETWEEN %s AND %s AND %s.%s=%s AND %s.%s!=%s %s',
$this->_getSelectFields($aliasFields),
$this->_addSQL($addSQL, 'cols'),
$this->node_table,
$this->_addSQL($addSQL, 'join'),
$this->node_table,
$this->flparams['l'],
$parent->l,
$parent->r,
$this->node_table,
$this->flparams['rootid'],
$this->db->quote($parent->rootid),
$this->node_table,
$this->flparams['id'],
$this->db->quote($id),
$this->_addSQL($addSQL, 'append'));
if (!$this->_caching) {
$nodeSet = $this->_processResultSet($sql, $keepAsArray, $aliasFields);
} else {
$nodeSet = $this->cache->call('DB_NestedSet->_processResultSet', $sql, $keepAsArray, $aliasFields);
}
// EVENT (nodeLoad)
foreach (array_keys($nodeSet) as $key) {
$this->triggerEvent('nodeLoad', $nodeSet[$key]);
}
return $nodeSet;
}
// }}}
// {{{ pickNode()
/**
* Fetch the data of a node with the given id
*
* @param int $id The node id of the node to fetch
* @param bool $keepAsArray (optional) Keep the result as an array or transform it into
* a set of DB_NestedSet_Node objects?
* @param bool $aliasFields (optional) Should we alias the fields so they are the names
* of the parameter keys, or leave them as is?
* @param string $idfield (optional) Which field has to be compared with $id?
* This is can be used to pick a node by other values (e.g. it's name).
* @param array $addSQL (optional) Array of additional params to pass to the query.
*
* @see _addSQL()
* @access public
* @return mixed False on error, or an array of nodes
*/
function pickNode($id, $keepAsArray = false, $aliasFields = true, $idfield = 'id', $addSQL = array())
{
$this->_debugMessage('pickNode($id)');
if (is_object($id) && $id->id) {
$id = $id->id;
}
$sql = sprintf('SELECT %s %s FROM %s %s WHERE %s.%s=%s %s',
$this->_getSelectFields($aliasFields),
$this->_addSQL($addSQL, 'cols'),
$this->node_table,
$this->_addSQL($addSQL, 'join'),
$this->node_table,
$this->flparams[$idfield],
$this->db->quote($id),
$this->_addSQL($addSQL, 'append'));
if (!$this->_caching) {
$nodeSet = $this->_processResultSet($sql, $keepAsArray, $aliasFields);
} else {
$nodeSet = $this->cache->call('DB_NestedSet->_processResultSet', $sql, $keepAsArray, $aliasFields);
}
$nsKey = false;
// EVENT (nodeLoad)
foreach (array_keys($nodeSet) as $key) {
$this->triggerEvent('nodeLoad', $nodeSet[$key]);
$nsKey = $key;
}
if (is_array($nodeSet) && $idfield != 'id') {
$id = $nsKey;
}
return isset($nodeSet[$id]) ? $nodeSet[$id] : false;
}
// }}}
// {{{ isParent()
/**
* See if a given node is a parent of another given node
*
* A node is considered to be a parent if it resides above the child
* So it doesn't mean that the node has to be an immediate parent.
* To get this information simply compare the levels of the two nodes
* after you know that you have a parent relation.
*
* @param mixed $parent The parent node as array or object
* @param mixed $child The child node as array or object
*
* @access public
* @return bool True if it's a parent
*/
function isParent($parent, $child) {
$this->_debugMessage('isParent($parent, $child)');
if (!isset($parent)|| !isset($child)) {
return false;
}
if (is_array($parent)) {
$p_rootid = $parent['rootid'];
$p_l = $parent['l'];
$p_r = $parent['r'];
} elseif (is_object($parent)) {
$p_rootid = $parent->rootid;
$p_l = $parent->l;
$p_r = $parent->r;
}
if (is_array($child)) {
$c_rootid = $child['rootid'];
$c_l = $child['l'];
$c_r = $child['r'];
} elseif (is_object($child)) {
$c_rootid = $child->rootid;
$c_l = $child->l;
$c_r = $child->r;
}
if (($p_rootid == $c_rootid) && ($p_l < $c_l && $p_r > $c_r)) {
return true;
}
return false;
}
// }}}
// {{{ _processResultSet()
/**
* Processes a DB result set by checking for a DB error and then transforming the result
* into a set of DB_NestedSet_Node objects or leaving it as an array.
*
* @param string $sql The sql query to be done
* @param bool $keepAsArray Keep the result as an array or transform it into a set of
* DB_NestedSet_Node objects?
* @param bool $fieldsAreAliased Are the fields aliased?
*
* @access private
* @return mixed False on error or the transformed node set.
*/
function _processResultSet($sql, $keepAsArray, $fieldsAreAliased)
{
$result = $this->db->getAll($sql);
if ($this->_testFatalAbort($result, __FILE__, __LINE__)) {
return false;
}
$nodes = array();
$idKey = $fieldsAreAliased ? 'id' : $this->flparams['id'];
foreach ($result as $row) {
$node_id = $row[$idKey];
if ($keepAsArray) {
$nodes[$node_id] = $row;
} else {
// Create an instance of the node container
$nodes[$node_id] =& new DB_NestedSet_Node($row);
}
}
return $nodes;
}
// }}}
// {{{ _getNodeObject()
/**
* Gets the node to work on based upon an id
*
* @param mixed $id The id which can be an object or integer
*
* @access private
* @return mixed The node object for an id or false on error
*/
function _getNodeObject($id)
{
if (!is_object($id) || !$id->id) {
return $this->pickNode($id);
}
else {
return $id;
}
}
// }}}
// {{{ _addSQL()
/**
* Adds a specific type of SQL to a query string
*
* @param array $addSQL The array of SQL strings to add. Example value:
* $addSQL = array(
* 'cols' => 'tb2.col2, tb2.col3', // Additional tables/columns
* 'join' => 'LEFT JOIN tb1 USING(STRID)', // Join statement
* 'append' => 'GROUP by tb1.STRID'); // Group condition
* @param string $type The type of SQL. Can be 'cols', 'join', or 'append'.
*
* @access private
* @return string The SQL, properly formatted
*/
function _addSQL($addSQL, $type)
{
if (!isset($addSQL[$type])) {
return '';
}
switch($type) {
case 'cols':
return ', ' . $addSQL[$type];
break;
default:
return $addSQL[$type];
break;
}
}
// }}}
// {{{ _getSelectFields()
/**
* Gets the select fields based on the params
*
* @param bool $aliasFields Should we alias the fields so they are the names of the
* parameter keys, or leave them as is?
*
* @access private
* @return string A string of query fields to select
*/
function _getSelectFields($aliasFields)
{
$queryFields = array();
foreach ($this->params as $key => $val) {
$tmp_field = $this->node_table . '.' . $key;
if ($aliasFields) {
$tmp_field .= ' AS ' . $val;
}
$queryFields[] = $tmp_field;
}
$fields = implode(', ', $queryFields);
return $fields;
}
// }}}
// +----------------------------------------------+
// | NestedSet manipulation and query methods |
// |----------------------------------------------+
// | insert / delete / update of nodes |
// +----------------------------------------------+
// | [PUBLIC] |
// +----------------------------------------------+
// {{{ createRootNode()
/**
* Creates a new root node
* Optionally it deletes the whole tree and creates one initial rootnode
*
* <pre>
* +-- root1 [target]
* |
* +-- root2 [new]
* |
* +-- root3
* </pre>
*
* @param array $values Hash with param => value pairs of the node (see $this->params)
* @param integer $id ID of target node (the rootnode after which the node should be inserted)
* @param bool $first Danger: Deletes and (re)init's the hole tree - sequences are reset
*
* @access public
* @return int The node id
*/
function createRootNode($values, $id = false, $first = false)
{
$this->_debugMessage('createRootNode($values, $id = false, $first = false)');
// Try to aquire a table lock
if(PEAR::isError($lock=$this->_setLock())) {
return $lock;
}
$flft = $this->flparams['l'];
$frgt = $this->flparams['r'];
$froot = $this->flparams['rootid'];
$fid = $this->flparams['id'];
$freh = $this->flparams['norder'];
$flevel = $this->flparams['level'];
$tb = $this->node_table;
$addval = array();
$addval[$flevel] = 1;
// Shall we delete the existing tree (reinit)
if ($first) {
$sql = "DELETE FROM $tb";
$this->db->query($sql);
$this->db->dropSequence($this->sequence_table);
// New order of the new node will be 1
$addval[$freh] = 1;
} else {
// Let's open a gap for the new node
$parent = $this->pickNode($id);
if (!$parent) {
// invalid parent node, order will be 1
$addval[$freh] = 1;
// no gap to make
$first = true;
}
else {
$addval[$freh] = $parent->norder + 1;
}
}
// Sequence of node id (equals to root id in this case
$addval[$froot] = $node_id = $addval[$fid] = $this->db->nextId($this->sequence_table);
// Left/Right values for rootnodes
$addval[$flft] = 1;
$addval[$frgt] = 2;
// Transform the node data hash to a query
if (!$qr = $this->_values2Query($values, $addval)) {
return false;
}
if (!$first) {
// Open the gap
$sql = "UPDATE $tb SET $freh=$freh+1 WHERE $fid=$froot AND $freh>$parent->norder";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
}
// Insert the new node
$sql = "INSERT INTO $tb SET $qr";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
// EVENT (nodeCreate)
$thisnode = &$this->pickNode($node_id);
$this->triggerEvent('nodeCreate', $thisnode);
return $node_id;
}
// }}}
// {{{ createSubNode()
/**
* Creates a subnode
*
* <pre>
* +-- root1
* |
* +-\ root2 [target]
* | |
* | |-- subnode1 [new]
* |
* +-- root3
* </pre>
*
* @param integer $id Parent node ID
* @param array $values Hash with param => value pairs of the node (see $this->params)
*
* @access public
* @return mixed The node id or false on error
*/
function createSubNode($id, $values)
{
$this->_debugMessage('createSubNode($id, $values)');
// Try to aquire a table lock
if(PEAR::isError($lock = $this->_setLock())) {
return $lock;
}
$freh = $this->flparams['norder'];
$flevel = $this->flparams['level'];
// Get the children of the target node
$children = $this->getChildren($id, false, true, true);
// We have children here
if ($children) {
// Get the last child
$last = array_pop($children);
// What we have to do is virtually an insert of a node after the last child
// So we don't have to proceed creating a subnode
$newNode =& $this->createRightNode($last->id, $values);
return $newNode;
}
// invalid parent id, bail out
if (!($thisnode = $this->pickNode($id))) {
$this->raiseError("Parent id: $id not found", NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_ERROR);
return false;
}
$flft = $this->flparams['l'];
$frgt = $this->flparams['r'];
$froot = $this->flparams['rootid'];
$fid = $this->flparams['id'];
$lft = $thisnode->l;
$rgt = $thisnode->r;
$rootid = $thisnode->rootid;
$plevel = $thisnode->level;
$tb = $this->node_table;
// Open the gap
$sql = "UPDATE $tb SET $flft=$flft+2
WHERE $froot=" . $this->db->quote($rootid) . " AND
$flft>" . $this->db->quote($rgt) . " AND
$frgt>=" . $this->db->quote($rgt);
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
$sql = "UPDATE $tb SET $frgt=$frgt+2
WHERE $froot=" . $this->db->quote($rootid) . " AND
$frgt>=" . $this->db->quote($rgt);
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
$addval = array();
$addval[$flft] = $rgt;
$addval[$frgt] = $rgt + 1;
$addval[$froot] = $rootid;
$addval[$freh] = 1;
$addval[$flevel] = $plevel + 1;
$node_id = $addval[$fid] = $this->db->nextId($this->sequence_table);
if (!$qr = $this->_values2Query($values, $addval)) {
return false;
}
$sql = "INSERT INTO $tb SET $qr";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
// EVENT (NodeCreate)
$thisnode = $this->pickNode($node_id);
$this->triggerEvent('nodeCreate', $thisnode);
return $node_id;
}
// }}}
// {{{ createRightNode()
/**
* Creates a node after a given node
* <pre>
* +-- root1
* |
* +-\ root2
* | |
* | |-- subnode1 [target]
* | |-- subnode2 [new]
* | |-- subnode3
* |
* +-- root3
* </pre>
*
* @param int $target Target node ID
* @param array $values Hash with param => value pairs of the node (see $this->params)
*
* @access public
* @return object The new node object
*/
function createRightNode($target, $values)
{
$this->_debugMessage('createRightNode($target, $values)');
if(PEAR::isError($lock=$this->_setLock())) {
return $lock;
}
$id = $target;
$flft = $this->flparams['l'];
$frgt = $this->flparams['r'];
$froot = $this->flparams['rootid'];
$freh = $this->flparams['norder'];
$fid = $this->flparams['id'];
$flevel = $this->flparams['level'];
// invalid target node, bail out
if (!($thisnode = $this->pickNode($id))) {
$this->raiseError("Target id: $id not found", NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_ERROR);
return false;
}
// If the target node is a rootnode we virtually want to create a new root node
if ($thisnode->rootid == $thisnode->id) {
return $this->createRootNode($values, $id);
}
$lft = $thisnode->l;
$rgt = $thisnode->r;
$rootid = $thisnode->rootid;
$level = $thisnode->level;
$parent_order = $thisnode->norder;
$tb = $this->node_table;
$addval = array();
$parents = $this->getParents($id);
$parent = array_pop($parents);
$plft = $parent->l;
$prgt = $parent->r;
// Open the gap within the current level
$sql = "UPDATE $tb SET $freh=$freh+1
WHERE $froot=" . $this->db->quote($rootid) . " AND
$freh>$parent_order AND
$flevel=$level AND
$flft BETWEEN $plft AND $prgt";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
// Update all nodes which have dependent left and right values
$sql = "UPDATE $tb SET
$flft=IF($flft>$rgt, $flft+2, $flft),
$frgt=IF($frgt>$rgt, $frgt+2, $frgt)
WHERE $froot=" . $this->db->quote($rootid) . "
AND $frgt>" . $this->db->quote($rgt);
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
$addval[$freh] = $parent_order + 1;
$addval[$flft] = $rgt + 1;
$addval[$frgt] = $rgt + 2;
$addval[$froot] = $rootid;
$addval[$flevel] = $level;
$node_id = $addval[$fid] = $this->db->nextId($this->sequence_table);
if (!$qr = $this->_values2Query($values, $addval)) {
return false;
}
// Insert the new node
$sql = "INSERT INTO $tb SET $qr";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
// EVENT (NodeCreate)
$thisnode =& $this->pickNode($node_id);
$this->triggerEvent('nodeCreate', $thisnode);
return $node_id;
}
// }}}
// {{{ deleteNode()
/**
* Deletes a node
*
* @param int $id ID of the node to be deleted
*
* @access public
* @return bool True if the delete succeeds
*/
function deleteNode($id)
{
$this->_debugMessage('deleteNode($id)');
if (PEAR::isError($lock = $this->_setLock())) {
return $lock;
}
if (!($thisnode = $this->pickNode($id))) {
return false;
}
// EVENT (NodeDelete)
$this->triggerEvent('nodeDelete', $thisnode);
$parents = $this->getParents($id);
$parent = array_pop($parents);
$plft = $parent->l;
$prgt = $parent->r;
$tb = $this->node_table;
$flft = $this->flparams['l'];
$frgt = $this->flparams['r'];
$fid = $this->flparams['id'];
$froot = $this->flparams['rootid'];
$freh = $this->flparams['norder'];
$flevel = $this->flparams['level'];
$lft = $thisnode->l;
$rgt = $thisnode->r;
$order = $thisnode->norder;
$level = $thisnode->level;
$rootid = $thisnode->rootid;
$len = $rgt - $lft + 1;
// Delete the node
$sql = "DELETE from $tb WHERE $flft BETWEEN $lft AND $rgt AND $froot=" . $this->db->quote($rootid);
$this->db->query($sql);
if ($thisnode->id != $thisnode->rootid) {
// The node isn't a rootnode so close the gap
$sql = "UPDATE $tb SET
$flft=IF($flft>$lft, $flft-$len, $flft),
$frgt=IF($frgt>$lft, $frgt-$len, $frgt)
WHERE $froot=" . $this->db->quote($rootid) . " AND
($flft>$lft OR $frgt>$rgt)";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
// Re-order
$sql = "UPDATE $tb SET $freh=$freh-1
WHERE $froot=" . $this->db->quote($rootid) . " AND
$flevel=$level AND
$freh>$order AND
$flft BETWEEN $plft AND $prgt";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
} else {
// A rootnode was deleted and we only have to close the gap inside the order
$sql = "UPDATE $tb SET $freh=$freh-1 WHERE $froot=$fid AND $freh > $order";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
}
return true;
}
// }}}
// {{{ updateNode()
/**
* Changes the payload of a node
*
* @param int $id Node ID
* @param array $values Hash with param => value pairs of the node (see $this->params)
*
* @access public
* @return bool True if the update is successful
*/
function updateNode($id, $values)
{
$this->_debugMessage('updateNode($id, $values)');
if (PEAR::isError($lock = $this->_setLock())) {
return $lock;
}
if (!($thisnode =& $this->pickNode($id))) {
return false;
}
$eparams = array('values' => $values);
// EVENT (NodeUpdate)
$this->triggerEvent('nodeUpdate', $thisnode, $eparams);
$fid = $this->flparams['id'];
$addvalues = array();
if (!$qr = $this->_values2Query($values, $addvalues)) {
return false;
}
$sql = "UPDATE $this->node_table SET $qr WHERE $fid=" . $this->db->quote($id);
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
return true;
}
// }}}
// +----------------------------------------------+
// | Moving and copying |
// |----------------------------------------------+
// | [PUBLIC] |
// +----------------------------------------------+
// {{{ moveTree()
/**
* Wrapper for node moving and copying
*
* @param int $id Source ID
* @param int $target Target ID
* @param array $pos Position (use one of the NESE_MOVE_* constants)
* @param bool $copy Shall we create a copy
*
* @see _moveInsideLevel
* @see _moveAcross
* @see moveRoot2Root
* @access public
* @return int ID of the moved node or false on error
*/
function moveTree($id, $target, $pos, $copy = false)
{
$this->_debugMessage('moveTree($id, $target, $pos, $copy = false)');
if (PEAR::isError($lock = $this->_setLock())) {
return $lock;
}
// This operations don't need callbacks except the copy handler
// which ignores this setting
$this->skipCallbacks = true;
// Get information about source and target
if (!($source = $this->pickNode($id))) {
$this->raiseError("Node id: $id not found", NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_ERROR);
return false;
}
if (!($target = $this->pickNode($target))) {
$this->raiseError("Target id: $target not found", NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_ERROR);
return false;
}
// We have a recursion - let's stop
if (($target->rootid == $source->rootid) &&
(($source->l <= $target->l) &&
($source->r >= $target->r))) {
return new PEAR_Error($this->_getMessage(NESE_ERROR_RECURSION),NESE_ERROR_RECURSION);
}
// Insert/move before or after
if ($pos == NESE_MOVE_BEFORE || $pos == NESE_MOVE_AFTER) {
if (($source->rootid == $source->id) &&
($target->rootid == $target->id) &&
!$copy) {
// We have to move a rootnode which is different from moving inside a tree
return $this->moveRoot2Root($source, $target, $pos, $copy);
}
if (($source->rootid == $target->rootid) &&
($source->level == $target->level)) {
// We have to move inside the same subtree and inside the same level - no big deal
return $this->_moveInsideLevel($source, $target, $pos, $copy);
}
}
// We have to move between different levels and maybe subtrees - let's rock ;)
return $this->_moveAcross($source, $target, $pos, $copy);
}
// }}}
// {{{ _moveAcross()
/**
* Moves nodes and trees to other subtrees or levels
*
* <pre>
* [+] <--------------------------------+
* +-[\] root1 [target] |
* <-------------------------+ |
* +-\ root2 | |
* | | | |
* | |-- subnode1 [target] | |B
* | |-- subnode2 [new] |S |E
* | |-- subnode3 |U |F
* | |B |O
* +-\ root3 | |R
* |-- subnode 3.1 | |E
* |-\ subnode 3.2 [source] >--+------+
* |-- subnode 3.2.1
*</pre>
*
* @param object NodeCT $source Source node
* @param object NodeCT $target Target node
* @param string $pos Position [SUBnode/BEfore]
* @param bool $copy Shall we create a copy
*
* @access private
* @see moveTree
* @see _r_moveAcross
* @see _moveCleanup
*/
function _moveAcross($source, $target, $pos, $copy = false)
{
$this->_debugMessage('_moveAcross($source, $target, $pos, $copy = false)');
if (PEAR::isError($lock = $this->_setLock())) {
return $lock;
}
$tb = $this->node_table;
$flft = $this->flparams['l'];
$frgt = $this->flparams['r'];
$fid = $this->flparams['id'];
$froot = $this->flparams['rootid'];
$freh = $this->flparams['norder'];
$s_id = $source->id;
$t_id = $target->id;
$rootid = $target->rootid;
// Get the current data from a node and exclude the id params which will be changed
// because of the node move
foreach($this->params as $key => $val) {
if ($source->$val && !in_array($val, $this->requiredParams)) {
$values[$key] = trim($source->$val);
}
}
if ($pos != NESE_MOVE_BELOW) {
$c_id = $this->createRightNode($t_id, $values);
$clone = $this->pickNode($c_id);
if ($pos == NESE_MOVE_BEFORE) {
$this->moveTree($c_id, $t_id, $pos);
}
} else {
$c_id = $this->createSubNode($t_id, $values);
$clone = $this->pickNode($c_id);
}
$relations[$s_id] = $c_id;
$children = $this->getChildren($source, false, true, true);
$first = true;
if ($children) {
// Recurse trough the child nodes
foreach($children AS $key => $val) {
if ($first) {
$first = false;
$previd = $this->_r_moveAcross($val, $clone, 'createSubNode', $relations);
} else {
$sister = $this->pickNode($previd);
$previd = $this->_r_moveAcross($val, $sister, 'createRightNode', $relations);
}
}
}
$this->_moveCleanup($relations, $copy);
if(!$copy) {
return $source->id;
} else {
return $clone->id;
}
}
// }}}
// {{{ _r_moveAcross()
/**
* Recursion for _moveAcross
*
* @param object NodeCT $source Source
* @param object NodeCT $target Target
* @param string $action createRightNode|createSubNode
* @param array $relations Hash $h[old ID]=new ID - maps the source node to the new created node (clone)
* @access private
* @see _moveAcross
*/
function _r_moveAcross($source, $target, $action, &$relations) {
$this->_debugMessage('_r_moveAcross($source, $target, $action, &$relations)');
if (PEAR::isError($lock = $this->_setLock())) {
return $lock;
}
foreach($this->params AS $key => $val) {
if ($source->$val && !in_array($val, $this->requiredParams)) {
$values[$key] = trim($source->$val);
}
}
$s_id = $source->id;
$t_id = $target->id;
$c_id = $this->$action($t_id, $values);
$relations[$s_id] = $c_id;
$children = $this->getChildren($source, false, true, true);
if (!$children) {
return $c_id;
}
$clone = $this->pickNode($c_id);
$first = true;
foreach($children as $key => $val) {
if ($first) {
$first = false;
$previd =
$this->_r_moveAcross($val, $clone, 'createSubNode', $relations);
} else {
$sister = $this->pickNode($previd);
$previd = $this->_r_moveAcross($val, $sister, 'createRightNode', $relations);
}
}
return $c_id;
}
// }}}
// {{{ _moveCleanup()
/**
* Deletes the old subtree (node) and writes the node id's into the cloned tree
*
*
* @param array $relations Hash in der Form $h[alteid]=neueid
* @param array $copy Are we in copy mode?
* @access private
*/
function _moveCleanup($relations, $copy = false)
{
$this->_debugMessage('_moveCleanup($relations, $copy = false)');
if (PEAR::isError($lock = $this->_setLock())) {
return $lock;
}
$tb = $this->node_table;
$fid = $this->flparams['id'];
$froot = $this->flparams['rootid'];
foreach($relations AS $key => $val) {
$clone = $this->pickNode($val);
if ($copy) {
// EVENT (NodeCopy)
$thisnode =& $this->pickNode($key);
$eparams = array('clone' => $clone);
$this->triggerEvent('nodeCopy', $thisnode, $eparams);
continue;
}
// No callbacks here because the node itself doesn't get changed
// Only it's position
// If one needs a callback here please let me know
$this->skipCallbacks = true;
$this->deleteNode($key, true);
// It's isn't a rootnode
if ($clone->id != $clone->rootid) {
$u_values = array();
$u_id = $val;
$u_values[$fid] = $key;
$this->updateNode($u_id, $u_values);
} else {
$sql = "UPDATE $tb SET
$fid=" . $this->db->quote($key) . ",
$froot=" . $this->db->quote($key) . "
WHERE $fid=" . $this->db->quote($val);
$this->db->query($sql);
$orootid = $clone->rootid;
$sql = "UPDATE $tb
SET $froot=" . $this->db->quote($key) . "
WHERE $froot=" . $this->db->quote($orootid);
$this->db->query($sql);
}
$this->skipCallbacks = false;
}
return true;
}
// }}}
// {{{ _moveInsideLevel()
/**
* Moves a node or subtree inside the same level
*
* <pre>
* +-- root1
* |
* +-\ root2
* | |
* | |-- subnode1 [target]
* | |-- subnode2 [new]
* | |-- subnode3
* |
* +-\ root3
* [|] <-----------------------+
* |-- subnode 3.1 [target] |
* |-\ subnode 3.2 [source] >--+
* |-- subnode 3.2.1
* </pre>
*
* @param object NodeCT $source Source
* @param object NodeCT $target Target
* @param string $pos BEfore | AFter
* @param string $copy Copy mode?
* @access private
* @see moveTree
*/
function _moveInsideLevel($source, $target, $pos, $copy = false)
{
$this->_debugMessage('_moveInsideLevel($source, $target, $pos, $copy = false)');
if (PEAR::isError($lock=$this->_setLock())) {
return $lock;
}
// If we only want to copy it's quite easy cause no gap will occur as in move mode
if ($copy) {
$parents = $this->getParents($target->id);
$ntarget = @array_pop($parents);
if (is_object($ntarget)) {
$npos = NESE_MOVE_BELOW;
} else {
$npos = $pos;
$ntarget = $target;
}
// Let's move the node to it's destination
$nroot = $this->_moveAcross($source, $ntarget, $npos, $copy);
// Change the order
return $this->moveTree($nroot, $target->id, $pos);
}
$parents = $this->getParents($source);
$parent = array_pop($parents);
$plft = $parent->l;
$prgt = $parent->r;
$tb = $this->node_table;
$flft = $this->flparams['l'];
$frgt = $this->flparams['r'];
$fid = $this->flparams['id'];
$froot = $this->flparams['rootid'];
$freh = $this->flparams['norder'];
$flevel = $this->flparams['level'];
$s_order = $source->norder;
$t_order = $target->norder;
$level = $source->level;
$rootid = $source->rootid;
$s_id = $source->id;
$t_id = $target->id;
if ($s_order < $t_order) {
if ($pos == NESE_MOVE_BEFORE) {
$sql = "UPDATE $tb SET $freh=$freh-1
WHERE $freh BETWEEN $s_order AND $t_order
AND
$fid!=$t_id
AND
$fid!=$s_id
AND
$flevel=" . $this->db->quote($level) . "
AND
$froot=" . $this->db->quote($rootid) . "
AND
$flft BETWEEN $plft AND $prgt";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
$sql = "UPDATE $tb SET $freh=$t_order-1 WHERE $fid=$s_id";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
}
elseif ($pos == NESE_MOVE_AFTER) {
$sql = "UPDATE $tb SET $freh=$freh-1
WHERE $freh BETWEEN $s_order AND $t_order
AND
$fid!=$s_id
AND
$flevel=" . $this->db->quote($level) . "
AND
$froot=" . $this->db->quote($rootid) . "
AND
$flft BETWEEN $plft AND $prgt";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
$sql = "UPDATE $tb SET $freh=$t_order WHERE $fid = $s_id";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
}
}
if ($s_order > $t_order) {
if ($pos == NESE_MOVE_BEFORE) {
$sql = "UPDATE $tb SET $freh=$freh+1
WHERE $freh BETWEEN $t_order AND $s_order
AND
$fid != $s_id
AND
$froot=" . $this->db->quote($rootid) . "
AND
$flevel=" . $this->db->quote($level) . "
AND
$flft BETWEEN $plft AND $prgt";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
$sql = "UPDATE $tb SET $freh=$t_order WHERE $fid=$s_id";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
}
elseif ($pos == NESE_MOVE_AFTER) {
$sql = "UPDATE $tb SET $freh=$freh+1
WHERE $freh BETWEEN $t_order AND $s_order
AND
$fid!=$t_id
AND
$fid!=$s_id
AND
$froot=" . $this->db->quote($rootid) . "
AND
$flevel=" . $this->db->quote($level) . "
AND
$flft BETWEEN $plft AND $prgt";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
$sql = "UPDATE $tb SET $freh=$t_order+1 WHERE $fid=$s_id";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
}
}
return $source->id;
}
// }}}
// {{{ moveRoot2Root()
/**
* Moves rootnodes
*
* <pre>
* +-- root1
* |
* +-\ root2
* | |
* | |-- subnode1 [target]
* | |-- subnode2 [new]
* | |-- subnode3
* |
* +-\ root3
* [|] <-----------------------+
* |-- subnode 3.1 [target] |
* |-\ subnode 3.2 [source] >--+
* |-- subnode 3.2.1
* </pre>
*
* @param object NodeCT $source Source
* @param object NodeCT $target Target
* @param object NodeCT $target Parent
* @param string $pos BEfore | AFter
* @param string $copy Copy mode?
* @access private
* @see moveTree
*/
function moveRoot2Root($source, $target, $pos, $copy)
{
$this->_debugMessage('moveRoot2Root($source, $target, $pos, $copy)');
if(PEAR::isError($lock=$this->_setLock())) {
return $lock;
}
$tb = $this->node_table;
$flft = $this->flparams['l'];
$frgt = $this->flparams['r'];
$fid = $this->flparams['id'];
$froot = $this->flparams['rootid'];
$freh = $this->flparams['norder'];
$s_order = $source->norder;
$t_order = $target->norder;
$s_id = $source->id;
$t_id = $target->id;
if ($s_order < $t_order) {
if ($pos == NESE_MOVE_BEFORE) {
$sql = "UPDATE $tb SET $freh=$freh-1
WHERE $freh BETWEEN $s_order AND $t_order AND
$fid!=$t_id AND
$fid!=$s_id AND
$froot=$fid";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
$sql = "UPDATE $tb SET $freh=$t_order -1 WHERE $fid=$s_id";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
}
elseif($pos == NESE_MOVE_AFTER) {
$sql = "UPDATE $tb SET $freh=$freh-1
WHERE $freh BETWEEN $s_order AND $t_order AND
$fid!=$s_id AND
$froot=$fid";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
$sql = "UPDATE $tb SET $freh=$t_order WHERE $fid=$s_id";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
}
}
if ($s_order > $t_order) {
if ($pos == NESE_MOVE_BEFORE) {
$sql = "UPDATE $tb SET $freh=$freh+1
WHERE $freh BETWEEN $t_order AND $s_order AND
$fid != $s_id AND
$froot=$fid";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
$sql = "UPDATE $tb SET $freh=$t_order WHERE $fid=$s_id";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
}
elseif ($pos == NESE_MOVE_AFTER) {
$sql = "UPDATE $tb SET $freh=$freh+1
WHERE $freh BETWEEN $t_order AND $s_order AND
$fid!=$t_id AND
$fid!=$s_id AND
$froot=$fid";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
$sql = "UPDATE $tb SET $freh=$t_order+1 WHERE $fid = $s_id";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
}
}
return $source->id;
}
// }}}
// +-----------------------+
// | Helper methods |
// +-----------------------+
// {{{ _testFatalAbort()
/**
* Error Handler
*
* Tests if a given ressource is a PEAR error object
* ans raises a fatal error in case of an error object
*
* @param object PEAR::Error $errobj The object to test
* @param string $file The filename wher the error occured
* @param int $line The line number of the error
* @return void
* @access private
*/
function _testFatalAbort($errobj, $file, $line)
{
if (!PEAR::isError($errobj)) {
return false;
}
$this->_debugMessage('_testFatalAbort($errobj, $file, $line)');
if ($this->debug) {
$message = $errobj->getUserInfo();
$code = $errobj->getCode();
$msg = "$message ($code) in file $file at line $line";
} else {
$msg = $errobj->getMessage();
$code = $errobj->getCode(); }
$this->raiseError($msg, $code, PEAR_ERROR_TRIGGER, E_USER_ERROR);
}
// }}}
// {{{ addListener()
/**
* Add an event listener
*
* Adds an event listener and returns an ID for it
*
* @param string $event The ivent name
* @param string $listener The listener object
* @return string
* @access public
*/
function addListener($event, &$listener)
{
$listenerID = uniqid('el');
$this->eventListeners[$event][$listenerID] =& $listener;
return $listenerID;
}
// }}}
// {{{ removeListener()
/**
* Removes an event listener
*
* Removes the event listener with the given ID
*
* @param string $event The ivent name
* @param string $listenerID The listener's ID
* @return bool
* @access public
*/
function removeListener($event, $listenerID)
{
unset($this->eventListeners[$event][$listenerID]);
return true;
}
// }}}
// {{{ triggerEvent()
/**
* Triggers and event an calls the event listeners
*
* @param string $event The Event that occured
* @param object node $node A Reference to the node object which was subject to changes
* @param array $eparams A associative array of params which may be needed by the handler
* @return bool
* @access public
*/
function triggerEvent($event, &$node, $eparams = false)
{
if ($this->skipCallbacks ||
!isset($this->eventListeners[$event]) ||
!is_array($this->eventListeners[$event]) ||
count($this->eventListeners[$event]) == 0) {
return false;
}
foreach($this->eventListeners[$event] as $key => $val) {
if (!method_exists($val, 'callEvent')) {
return new PEAR_Error($this->_getMessage(NESE_ERROR_NOHANDLER), NESE_ERROR_NOHANDLER);
}
$val->callEvent($event, $node, $eparams);
}
return true;
}
// }}}
// {{{ setAttr()
/**
* Sets an object attribute
*
* @param array $attr An associative array with attributes
*
* @return bool
* @access public
*/
function setAttr($attr)
{
static $hasSetSequence;
if (!isset($hasSetSequence)) {
$hasSetSequence = false;
}
if (!is_array($attr) || count($attr) == 0) {
return false;
}
foreach ($attr as $key => $val) {
$this->$key = $val;
if ($key == 'sequence_table') {
$hasSetSequence = true;
}
// only update sequence to reflect new table if they haven't set it manually
if (!$hasSetSequence && $key == 'node_table') {
$this->sequence_table = $this->node_table . '_' . $this->flparams['id'];
}
if($key == 'cache' && is_object($val)) {
$this->_caching = true;
$GLOBALS['DB_NestedSet'] = & $this;
}
}
return true;
}
// }}}
// {{{ setDbOption()
/**
* Sets a db option. Example, setting the sequence table format
*
* @var string $option The option to set
* @var string $val The value of the option
*
* @access public
* @return void
*/
function setDbOption($option, $val)
{
$this->db->setOption($option, $val);
}
// }}}
// {{{ testLock()
/**
* Tests if a database lock is set
*
* @access public
*/
function testLock()
{
$this->_debugMessage('testLock()');
if($lockID = $this->structureTableLock) {
return $lockID;
}
$this->_lockGC();
$tb = $this->lock_table;
$stb = $this->node_table;
$lockTTL = time() - $this->lockTTL;
$sql = "SELECT lockID FROM $tb WHERE lockTable=" . $this->db->quote($stb);
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
if ($res->numRows()) {
return new PEAR_Error($this->_getMessage(NESE_ERROR_TBLOCKED),NESE_ERROR_TBLOCKED);
}
return false;
}
// }}}
// {{{ _setLock()
/**
* @access private
*/
function _setLock()
{
$lock = $this->testLock();
if(PEAR::isError($lock)) {
return $lock;
}
$this->_debugMessage('_setLock()');
if($this->_caching) {
@$this->cache->flush('function_cache');
$this->_caching = false;
$this->_restcache = true;
}
$tb = $this->lock_table;
$stb = $this->node_table;
$stamp = time();
if (!$lockID = $this->structureTableLock) {
$lockID = $this->structureTableLock = uniqid('lck-');
$sql = "INSERT INTO $tb SET
lockID=" . $this->db->quote($lockID) . ",
lockTable=" . $this->db->quote($stb) . ",
lockStamp=" . $this->db->quote($stamp);
} else {
$sql = "UPDATE $tb SET lockStamp=" . $this->db->quote($stamp) . "
WHERE lockID=" . $this->db->quote($lockID) . " AND
lockTable=" . $this->db->quote($stb);
}
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
return $lockID;
}
// }}}
// {{{ _releaseLock()
/**
* @access private
*/
function _releaseLock()
{
$this->_debugMessage('_releaseLock()');
if (!$lockID = $this->structureTableLock) {
return false;
}
$tb = $this->lock_table;
$stb = $this->node_table;
$sql = "DELETE FROM $tb
WHERE lockTable=" . $this->db->quote($stb) . " AND
lockID=" . $this->db->quote($lockID);
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
$this->structureTableLock = false;
if($this->_restcache) {
$this->_caching = true;
$this->_restcache = false;
}
return true;
}
// }}}
// {{{ _lockGC()
/**
* @access private
*/
function _lockGC()
{
$this->_debugMessage('_lockGC()');
$tb = $this->lock_table;
$stb = $this->node_table;
$lockTTL = time() - $this->lockTTL;
$sql = "DELETE FROM $tb
WHERE lockTable=" . $this->db->quote($stb) . " AND
lockStamp < $lockTTL";
$res = $this->db->query($sql);
$this->_testFatalAbort($res, __FILE__, __LINE__);
}
// }}}
// {{{ _values2Query()
/**
* @access private
*/
function _values2Query($values, $addval = false)
{
$this->_debugMessage('_values2Query($values, $addval = false)');
if (is_array($addval)) {
$values = $values + $addval;
}
$arq = array();
foreach($values AS $key => $val) {
$k = trim($key);
$v = trim($val);
if ($k) {
$arq[] = "$k=" . $this->db->quote($v);
}
}
if (!is_array($arq) || count($arq) == 0) {
return false;
}
$query = implode(', ', $arq);
return $query;
}
// }}}
// {{{ _debugMessage()
/**
* @access private
*/
function _debugMessage($msg)
{
if ($this->debug) {
$time = $this->_getmicrotime();
echo "$time::Debug:: $msg<br />\n";
}
}
// }}}
// {{{ _getMessage()
/**
* @access private
*/
function _getMessage($code)
{
$this->_debugMessage('_getMessage($code)');
return isset($this->messages[$code]) ? $this->messages[$code] : $this->messages[NESE_MESSAGE_UNKNOWN];
}
// }}}
// {{{ _getmicrotime()
/**
* @access private
*/
function _getmicrotime()
{
list($usec, $sec) = explode(' ', microtime());
return ((float)$usec + (float)$sec);
}
// }}}
}
// {{{ DB_NestedSet_Node:: class
/**
* Generic class for node objects
*
* @autor Daniel Khan <dk@webcluster.at>;
* @version $Revision: 1.31 $
* @package DB_NestedSet
*
* @access private
*/
// }}}
class DB_NestedSet_Node {
// {{{ constructor
/**
* Constructor
*/
function DB_NestedSet_Node($data)
{
if (!is_array($data) || count($data) == 0) {
return new PEAR_ERROR($data, NESE_ERROR_PARAM_MISSING);
}
$this->setAttr($data);
return true;
}
// }}}
// {{{ setAttr()
function setAttr($data)
{
if(!is_array($data) || count($data) == 0) {
return false;
}
foreach ($data as $key => $val) {
$this->$key = $val;
}
}
// }}}
}
?>